package model.market;

//AMXX: autoboxing has been used without thinking much... we may be doing silly things.
//******************************************
//AM: http://www.economist.com/finance/displaystory.cfm?story_id=10881318
//AMXX: introduce market cap figures and alter index numbers to be market cap adjusted.
//AMXX: momenutm due to information leakage management failure, feedback in portfolio rebalancing. other studies suggested newswatchers.
//AM: oil trading http://www.nzherald.co.nz/section/3/story.cfm?c_id=3&objectid=10521862
//******************************************
//AMLOW: dont spin the full portfolio mgmt evolve on first period because portfolio issued at asking price.

//AMXX:  create concept of periods in day.. vol etc stats will only be built off end of day figures. probably have 
//		same number of periods in day as number of assets and allow only keenest trade for each asset at each trade.
//				---- Correction.. what we want is a space between news, i.e. div levels get fixed for certain number of periods, plus we need to mod the stats collection
//						so that correlations can be drawn over more than one period... in fact that is really the key thing.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import model.trader.Investor;
import model.trader.MarketMaker;
import model.trader.Trader;
import model.util.RandomStandardNormal;

public class MarketManager {

	private MarketListener marketListener;

	public void setMarketListener(MarketListener marketListener) {
		this.marketListener = marketListener;
	}

	private MarketConfig config = new MarketConfig();
	private List<Trader> traders = new ArrayList<Trader>();
	private Map<Trader, Position> positions = new HashMap<Trader, Position>();
	private BankOfCredit bank = new BankOfCredit();

	private DayMarketInfo[] results;
	private int period;
	private AssetTradeRequests[] currentRequests;
	private double currentRiskFree;

	private List<RetiredTrader> tradersToDeregister = new ArrayList<RetiredTrader>();
	private List<Trader> deregisteredTraders = new ArrayList<Trader>();

	private double[] marketPrices;
	private RandomStandardNormal random = new RandomStandardNormal();

	public boolean newDawn() {
		return getConfig().newDawn(period);
	}

	/**
	 * This is the key algorithm in the system. The market runs over a number of
	 * iterations, gathering traders orders and matching them each time.
	 * 
	 * @return
	 */
	public ModelResults execute() {

		currentRiskFree = getConfig().getInitialRiskFreeRate();

		double[] annualisedDividends = prepareVariablesForRun();

		MarketInformation info = new MarketInformation(config);

		for (period = 0; period < getConfig().getIterations(); period++) {
			DayMarketInfo today = prepareVariablesForIteration(info);

			setDividends(annualisedDividends, today);

			PeriodResults[] tradeResults = new PeriodResults[getConfig().getNumberOfAssets()];

			if (period >= getConfig().getTradeDelay()) {

				getTradersOrders(info, newDawn());

				matchOrders(info, today, tradeResults);

				clearingAndSettlement(tradeResults, today, info);

			} else {
				setDummyMarketPrices(annualisedDividends, today);
			}

			dividendsAndCharges(today, info);

			today.setTradeClassFigures(positions, marketPrices);

			removeBankruptTradersAndPrepareTomorrowsBankruptcies(today);

			info.endOfDay(marketPrices, today.getCashflowPerShare(), today.getVolume(), currentRiskFree);

		}
		return new ModelResults(results, info);
	}

	private void setDividends(double[] yesterdaysDividends, DayMarketInfo today) {
		double time = getConfig().getIterations() / getConfig().getPeriodsInYear();
		double volatility = getConfig().getVolatility();
		double volatility0 = getConfig().getVolatilityAsset0();
		List<Double>[] numbers = random.getNumbers(getConfig().getSeedRandom(), getConfig().getNumberOfAssets());

		// AM: note the 20 is value shock wait time.
		if (period <= getConfig().getTradeDelay() || period == (getConfig().getTradeDelay() + 20)) {
			// double linkShift =0;
			setCashflow(volatility, volatility0, time, numbers, yesterdaysDividends, today);
		} else if (newDawn()) {
			setCashflow(volatility * Math.sqrt(getConfig().getTradesPerDay()), volatility0
					* Math.sqrt(getConfig().getTradesPerDay()), time, numbers, yesterdaysDividends, today);
		} else {
			// AM: set cash stuff to be same as yesterday; easy way of
			// saying no change.
			for (int assetCount = 0; assetCount < getConfig().getNumberOfAssets(); assetCount++) {
				today.setAnnualisedDividend(assetCount, yesterdaysDividends[assetCount]);
			}
		}
	}

	private double[] prepareVariablesForRun() {
		marketPrices = new double[getConfig().getNumberOfAssets()];
		double[] yesterdaysDividends = new double[getConfig().getNumberOfAssets()];
		Arrays.fill(marketPrices, 10);
		Arrays.fill(yesterdaysDividends, 10 * getConfig().getShareCashflowRate());
		results = new DayMarketInfo[getConfig().getIterations()];
		return yesterdaysDividends;
	}

	private DayMarketInfo prepareVariablesForIteration(MarketInformation info) {
		System.out.println("Period " + period);
		if (marketListener != null) {
			marketListener.newIteration(period);
		}

		currentRequests = new AssetTradeRequests[config.getNumberOfAssets()];
		for (int i = 0; i < currentRequests.length; i++) {
			currentRequests[i] = new AssetTradeRequests();
		}

		RateProvider riskFreeRateProvider = getConfig().getRiskFreeRateProvider();
		if (riskFreeRateProvider != null) {
			currentRiskFree = riskFreeRateProvider.getRate(currentRiskFree, period, info);
		}

		DayMarketInfo today = new DayMarketInfo(getConfig().getNumberOfAssets());
		results[period] = today;

		currentRequests = new AssetTradeRequests[config.getNumberOfAssets()];
		for (int i = 0; i < currentRequests.length; i++) {
			currentRequests[i] = new AssetTradeRequests();
		}

		return today;
	}

	private void setDummyMarketPrices(double[] dividends, DayMarketInfo today) {
		for (int assetIndex = 0; assetIndex < getConfig().getNumberOfAssets(); assetIndex++) {
			double marketPrice = dividends[assetIndex]
			                               / ((getConfig().getShareCashflowRate() - getConfig().getInitialRiskFreeRate() + currentRiskFree));
			today.setMarketPrice(assetIndex, marketPrice);
			// System.out.println("SEtting market price to:
			// "+simplePerpetuityValues[assetIndex]);
			marketPrices[assetIndex] = marketPrice;
		}
	}

	private void matchOrders(MarketInformation info, DayMarketInfo today, PeriodResults[] tradeResults) {
		for (int assetIndex = 0; assetIndex < getConfig().getNumberOfAssets(); assetIndex++) {

			sortTradeOrders(currentRequests[assetIndex]);
			MarketEngine engine = // new AuctionEngine();
				period == getConfig().getTradeDelay() ? new PortfolioInitEngine() : new AuctionEngine();// new
				// DealerEngine();

				try {
					tradeResults[assetIndex] = engine.executeOrders(period, currentRequests[assetIndex],
							marketPrices[assetIndex]);
					today.setMarketPrice(assetIndex, tradeResults[assetIndex].getMarketPrice());
					marketPrices[assetIndex] = tradeResults[assetIndex].getMarketPrice();
				} catch (MarketException e) {
					e.setStats(new ModelResults(results, info));
					throw e;
				}
		}
	}

	private void setCashflow(double volatility, double volatility0, double time, List<Double>[] numbers,
			double[] dividends, DayMarketInfo today) {
		for (int assetCount = 0; assetCount < getConfig().getNumberOfAssets(); assetCount++) {

			double oldDividend = dividends[assetCount];

			double powerOfE = // (currentRiskFree
				// -.5*variance)*time/iterations+
				volatility * Math.sqrt(time / getConfig().getIterations()) * numbers[assetCount].get(period);

			if (assetCount == 0) {
				// AM: configure risky and less risky assets. - interesting
				// stuff because they get favoured and then beta increases.
				powerOfE = volatility0 * Math.sqrt(time / getConfig().getIterations())
				* numbers[assetCount].get(period);
			}

			dividends[assetCount] = oldDividend * Math.exp(powerOfE);

			if (assetCount == 0 && period == (getConfig().getTradeDelay() + 20)) {
				dividends[assetCount] *= (1 + getConfig().getAsset0Shock());
			}

			today.setAnnualisedDividend(assetCount, dividends[assetCount]);
		}
	}

	private void removeBankruptTradersAndPrepareTomorrowsBankruptcies(DayMarketInfo today) {
		// First get rid of yesterdays bankrupts..
		int retirementCount = removeDeregisteredTraders(tradersToDeregister);
		today.setTradersRetired(retirementCount);

		tradersToDeregister = new ArrayList<RetiredTrader>();

		// failed traders will be prepared for removal post tomorrows trading.
		// not removed immediately because their position needs to be closed.
		int poppedTraderCount = markFailedTradersForDeReg(today);
		today.setTradersBlown(poppedTraderCount);
	}

	private void clearingAndSettlement(PeriodResults[] results, DayMarketInfo today, MarketInformation info) {
		if (getPeriod() >= getConfig().getTradeDelay()) {
			for (int i = 0; i < results.length; i++) {
				if (results[i] != null) {
					updatePositions(i, results[i].getExecutions(), today);
				}
			}
		}

	}

	// AMLOW: this stuff really isnt very elegant.. doing work duplicated in
	// marketinformation.
	// AM: important to note that market information is out of date.
	private void dividendsAndCharges(DayMarketInfo today, MarketInformation info) {

		double totalIndexFlow = 0;
		double indexPrice = 0;
		double[] shareCashflow = new double[getConfig().getNumberOfAssets()];
		// Dividends
		for (int assetCount = 0; assetCount < getConfig().getNumberOfAssets(); assetCount++) {

			// pay dividends, interest..
			double cfPerShare = today.getAnnualisedDividend(assetCount) / getConfig().getPeriodsInYear();
			shareCashflow[assetCount] = cfPerShare;
			today.setCashflowPerShare(assetCount, cfPerShare);
			totalIndexFlow += cfPerShare;
			indexPrice += marketPrices[assetCount];
		}

		double indexDivYield = (totalIndexFlow) * config.getPeriodsInYear() / indexPrice;

		double indexCapitalReturn = 0;
		if (info.getIndexPrices().size() != 0) {
			double oldIndexPrice = info.getIndexPrices().get(info.getIndexPrices().size() - 1);
			indexCapitalReturn = (indexPrice - oldIndexPrice) / oldIndexPrice;
		}

		// /Then we add a finance charge to each user, so that we know we are
		// measuring value added:
		Iterator<Trader> keys = positions.keySet().iterator();
		while (keys.hasNext()) {
			Trader trader = keys.next();
			Position pos = positions.get(trader);
			if (deregisteredTraders.contains(trader)) {
				continue;
			}
			// finance charge..AM related to current worth.

			// AMLOW: think about order of finance charges. what are we paying
			// for? This is after todays settlement clearance isnt it?
			double financeCharge = getConfig().getFinanceCharge().getFinanceCharge().getPeriodFinanceCharge(pos, info,
					this, currentRiskFree, indexDivYield, indexCapitalReturn);

			// Interest on bank deposit..
			pos.addCash(financeCharge + (pos.getCash() * (currentRiskFree) / getConfig().getPeriodsInYear()));

			if(period==getConfig().getTradeDelay()+30){
				if(trader instanceof Investor&& getConfig().getSubtractFractionShock()!=0){
					double subtraction=(pos.getWorth(marketPrices)*getConfig().getSubtractFractionShock());
					System.out.println("Subtracting "+subtraction);
					pos.addCash(subtraction);
				}
			}
		}

		// Pay Dividends last::
		for (int assetCount = 0; assetCount < getConfig().getNumberOfAssets(); assetCount++) {

			// pay dividends
			Iterator<Trader> traders = positions.keySet().iterator();
			while (traders.hasNext()) {
				Trader trader = traders.next();
				if (deregisteredTraders.contains(trader)) {
					continue;
				}
				Position pos = positions.get(trader);

				pos.addCash((pos.getExecutedQuantity(assetCount) * shareCashflow[assetCount]));


			}
		}

	}

	private int markFailedTradersForDeReg(DayMarketInfo today) {
		int poppedTraderCount = 0;
		Iterator<Trader> traderIterator = positions.keySet().iterator();
		while (traderIterator.hasNext()) {
			Trader trader = traderIterator.next();

			Position pos = positions.get(trader);
			// AM: shouldnt margin call opportunists either.?
			if (pos.getWorth(today.getMarketPrices()) < 0 && !(trader instanceof MarketMaker)) {
				// Popped!!
				System.out.println("Trader: " + trader.getClass() + " popped,  position: " + pos.getCash()
						+ " stock 0: " + pos.getExecutedQuantity(0));
				deregister(trader, false);
				poppedTraderCount++;
			}

			if (!(trader instanceof MarketMaker)) {
				// AM: check for negative holdings.. shouldnt happen as we dont
				// allow shorting..
				for (int i = 0; i < getConfig().getNumberOfAssets(); i++) {
					if (pos.getExecutedQuantity(i) < 0) {

						throw new RuntimeException("Trader has negative holding of " + i + "  trader: "
								+ trader.getClass() + " " + trader + " period " + period + " holding: "
								+ pos.getExecutedQuantity(i) + " worth: " + pos.getWorth(marketPrices) + " price: "
								+ marketPrices[i]);
					}
				}
			}

		}
		return poppedTraderCount;
	}

	private int removeDeregisteredTraders(List<RetiredTrader> tradersToDeregister) {
		int retirementCount = 0;
		for (int deregCount = 0; deregCount < tradersToDeregister.size(); deregCount++) {
			RetiredTrader retire = tradersToDeregister.get(deregCount);
			Trader dereg = retire.trader;
			if (retire.retiredNotPushed) {
				retirementCount++;
			}
			// traders.remove(dereg);
			deregisteredTraders.add(dereg);
			// positions.remove(dereg);

		}
		return retirementCount;
	}

	private void updatePositions(int assetReference, List<Trade> executions, DayMarketInfo today) {

		int totalVolume = 0;
		for (Trade trade : executions) {
			Position pos = positions.get(trade.getInitiator());
			pos.addTrade(trade);
			if (trade.getDirection() == Trade.BUY) {
				totalVolume += trade.getExecutedQuantity();
			}
		}

		// System.out.println("Iteration "+i+" total vol:"+totalVolume);
		if (period > getConfig().getTradeDelay()) {// NOte the first trade day
			// is skipped.
			today.setVolume(assetReference, totalVolume);
		}
	}

	private void sortTradeOrders(AssetTradeRequests audit) {
		Collections.sort(audit.getDailySells(), new TradeComparator());
		Collections.sort(audit.getDailyBuys(), new TradeComparator());
	}

	private void getTradersOrders(MarketInformation info, boolean checkMargins) {
		tradeLoop: for (int traderCount = 0; traderCount < traders.size(); traderCount++) {
			Trader trader = traders.get(traderCount);
			if (deregisteredTraders.contains(trader)) {
				continue;
			}
			for (int deregCount = 0; deregCount < tradersToDeregister.size(); deregCount++) {

				if (tradersToDeregister.get(deregCount).trader.equals(trader)) {
					// skip him..hes been popped!!
					continue tradeLoop;
				}
			}
			Position pos = positions.get(trader);

			if (checkMargins && bank.marginCall(marketPrices, pos)) {
				System.out.println("Margin call!!!   position: " + pos.getCash() + " worth "
						+ pos.getWorth(marketPrices));
				// Margin call!
				closeOrder(pos);
			} else {
				// AMLOW: dirty code switching the instance variable around.
				List<Trade> trades = trader.trade(this, pos, info);

				if (trades != null) {
					if (config.isAllowAllAssetTrades() || trader instanceof MarketMaker
							|| period == getConfig().getTradeDelay()) {
						for (Trade trade : trades) {
							// if(biggest==null||trade.get)
							order(trade);
						}
					} else {
						identifyAndTradeOnlyTheMostImportantAsset(trades);
					}
				}

			}
		}
	}

	private void identifyAndTradeOnlyTheMostImportantAsset(List<Trade> trades) {
		double[] totalTradeQuantity = new double[getConfig().getNumberOfAssets()];
		double[] closestTradePrice = new double[getConfig().getNumberOfAssets()];
		boolean[] sell = new boolean[getConfig().getNumberOfAssets()];
		Arrays.fill(closestTradePrice, 10);

		for (Trade trade : trades) {
			double limitPrice = trade.getLimitPrice();
			double diff = Math.abs(1 - limitPrice / marketPrices[trade.getAssetReference()]);

			// AM: not as straightforward as sell first.. need to make sure
			// tiddler trades arent ruining it.

			// AM: this code is dodgy.. mixing no cap orders with capped orders.
			if (trade.getLimitPrice() == 0 || diff < closestTradePrice[trade.getAssetReference()]) {
				if (trade.getDirection() == Trade.SELL) {
					sell[trade.getAssetReference()] = true;
				}
				// Lo mas circa el precio..
				totalTradeQuantity[trade.getAssetReference()] = marketPrices[trade.getAssetReference()]
				                                                             * trade.getQuantity();
				closestTradePrice[trade.getAssetReference()] = trade.getLimitPrice() == 0 ? 0 : diff;
			}
		}

		double maxAsset = 0;
		int maxAssetReference = 0;
		for (int i = 0; i < getConfig().getNumberOfAssets(); i++) {
			// System.out.print(" [ " + i + ":"
			// + totalTradeQuantity[i] + " closeness: "
			// + closestTradePrice[i] + " ] ");
			// AM: selling is twice as important as buying....
			if (totalTradeQuantity[i] > maxAsset || sell[i] && 2 * totalTradeQuantity[i] > maxAsset) {
				maxAsset = sell[i] ? 2 * totalTradeQuantity[i] : totalTradeQuantity[i];
				maxAssetReference = i;
			}
		}
		// System.out.print("		");

		boolean first = true;

		// Trade the most highly selected asset.
		for (Trade trade : trades) {
			if (trade.getAssetReference() == maxAssetReference) {
				order(trade);
				if (first) {
					first = false;
					// System.out.println("Trading
					// "+maxAssetReference+" current has
					// "+pos.getExecutedQuantity(maxAssetReference)+"
					// wants "+trade.getQuantity()+" at
					// "+trade.getLimitPrice());
				}
			}
		}
	}

	protected void order(Trade trade) {

		if (trade.getQuantity() == 0) {
			return;
		}
		if (trade.getDirection() == Trade.BUY) {
			if (!currentRequests[trade.getAssetReference()].getDailyBuys().contains(trade)) {
				currentRequests[trade.getAssetReference()].getDailyBuys().add(trade);
			}
		} else if (!currentRequests[trade.getAssetReference()].getDailySells().contains(trade)) {
			currentRequests[trade.getAssetReference()].getDailySells().add(trade);
		}

	}

	public void closeOrder(Position position) {
		if (position.getExecutedTrades().size() == 0) {
			throw new RuntimeException("Attempt to close unused position!");
		}
		Trader trader = position.getExecutedTrades().get(0).getInitiator();
		for (int i = 0; i < getConfig().getNumberOfAssets(); i++) {
			int quantity = position.getExecutedQuantity(i);

			if (quantity == 0) {
				// No need to trade.
				continue;
			}
			Trade cancel = new Trade(trader, i, -quantity);
			order(cancel);
		}
	}

	public void deregister(Trader trader, boolean retiredNotPushed) {
		closeOrder(positions.get(trader));
		tradersToDeregister.add(new RetiredTrader(retiredNotPushed, trader));

	}

	public void register(Trader trader, Position pos) {
		register(0, trader, pos);
	}

	public void register(int period, Trader trader, Position pos) {
		if (pos == null) {
			throw new RuntimeException("Null position given on trader registration.");
		}
		// AM: note index allocation wont be given so they must buy in.
		pos.setStartPeriod(period);
		traders.add(trader);
		positions.put(trader, pos);
	}

	public int getPeriod() {
		return period;
	}

	public MarketConfig getConfig() {
		return config;
	}

	public void setConfig(MarketConfig config) {
		this.config = config;
	}

}
